ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / feature / firmware / src / commonTest / kotlin / org / meshtastic / feature / firmware / ota / BleOtaTransportTest.kt
Displaying Raw β’ Download
feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/BleOtaTransportTest.kt 9fa9f1e341ee8d2c8fd9fabf9253b2f012fc8c95 (9fa9f1e3) Text, 24.63 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72package T7ee787org.meshtastic.feature.firmware.ota
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants.OTA_NOTIFY_CHARACTERISTIC
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants.OTA_WRITE_CHARACTERISTIC
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleConnection
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleDevice
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleScanner
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertTrue
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364BleOtaTransportTest Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffAA:BB:CC:DD:EE:FFTa5d6ff"
Tff7b72private Tff7b72fun Td2a8ffcreateTransportTb4b4b4(
Te6edf3scannerTb4b4b4: Te6edf3FakeBleScanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3connectionTb4b4b4: Te6edf3FakeBleConnection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3seedOtaCharacteristicsTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3TripleTff7b72<Te6edf3BleOtaTransportTb4b4b4, Te6edf3FakeBleScannerTb4b4b4, Te6edf3FakeBleConnectionTff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3seedOtaCharacteristicsTb4b4b4) Tb4b4b4{
T8b949e// Seed at the choke point instead of every connect() site β the new
T8b949e// service.requireOtaCharacteristics() validation in BleOtaTransport.connect() rejects
T8b949e// services missing these, so default to present; negative tests opt out.
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3addCharacteristicTb4b4b4(Te6edf3OTA_NOTIFY_CHARACTERISTICTb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3addCharacteristicTb4b4b4(Te6edf3OTA_WRITE_CHARACTERISTICTb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3transport Tff7b72=
Te6edf3BleOtaTransportTb4b4b4(
Te6edf3scanner Tff7b72= Te6edf3scannerTb4b4b4,
Te6edf3connectionFactory Tff7b72= Te6edf3FakeBleConnectionFactoryTb4b4b4(Te6edf3connectionTb4b4b4)Tb4b4b4,
Te6edf3address Tff7b72= Te6edf3addressTb4b4b4,
Te6edf3dispatcher Tff7b72= Te6edf3kotlinxTb4b4b4.Te6edf3coroutinesTb4b4b4.Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)
Tff7b72return Te6edf3TripleTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Tb4b4b4}
T8b949e/**
* Connect and prepare the transport for OTA operations. Must be called before [startOta] or [streamFirmware] tests.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffconnectTransportTb4b4b4(
Te6edf3transportTb4b4b4: Te6edf3BleOtaTransportTb4b4b4,
Te6edf3scannerTb4b4b4: Te6edf3FakeBleScannerTb4b4b4,
Te6edf3connectionTb4b4b4: Te6edf3FakeBleConnectionTb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3connectionTb4b4b4.Te6edf3maxWriteValueLength Tff7b72= T79c0ff5T79c0ff1T79c0ff2
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffconnect() must succeed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Emit a text response on the OTA notify characteristic. Because the notification observer from [connect] runs on
* [Dispatchers.Unconfined], the emission is delivered synchronously to [BleOtaTransport.responseChannel].
*/
Tff7b72private Tff7b72fun Td2a8ffemitResponseTb4b4b4(Te6edf3connectionTb4b4b4: Te6edf3FakeBleConnectionTb4b4b4, Te6edf3textTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3emitNotificationTb4b4b4(Te6edf3OTA_NOTIFY_CHARACTERISTICTb4b4b4, Te6edf3textTb4b4b4.Te6edf3encodeToByteArrayTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// -----------------------------------------------------------------------
T8b949e// connect()
T8b949e// -----------------------------------------------------------------------
Tf0883e@Test
Tff7b72fun Td2a8ff`connect succeeds when device is found`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`connect succeeds when device advertises MAC plus one`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// MAC+1 of AA:BB:CC:DD:EE:FF wraps last byte: FFβ00
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Ta5d6ff"Ta5d6ffAA:BB:CC:DD:EE:00Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`connect fails when connectAndAwait returns Disconnected`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3failNextN Tff7b72= T79c0ff1
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`connect fails when OTA characteristics are missing`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3seedOtaCharacteristics Tff7b72= Tff7b72falseTb4b4b4)
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3exception Tff7b72= Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTff7b72>Tb4b4b4(Te6edf3exceptionTb4b4b4)
Tff7b72val Te6edf3message Tff7b72= Te6edf3exceptionTb4b4b4.Te6edf3messageTb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3messageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffOTA serviceTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3messageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffTX notify characteristicTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3messageTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffOTA write characteristicTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`connect fails when notification observation fails before subscription`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3failure Tff7b72= Te6edf3IllegalStateExceptionTb4b4b4(Ta5d6ff"Ta5d6ffobserve failed before CCCDTa5d6ff"Tb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3observeBeforeSubscriptionExceptionByCharacteristicTff7b72[Te6edf3OTA_NOTIFY_CHARACTERISTICTff7b72] Tff7b72= Te6edf3failure
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Tff7b72val Te6edf3exception Tff7b72= Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3ConnectionFailedTff7b72>Tb4b4b4(Te6edf3exceptionTb4b4b4)
Tff7b72val Te6edf3cause Tff7b72= Te6edf3assertIsTff7b72<Te6edf3IllegalStateExceptionTff7b72>Tb4b4b4(Te6edf3exceptionTb4b4b4.Te6edf3causeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3failureTb4b4b4.Te6edf3messageTb4b4b4, Te6edf3causeTb4b4b4.Te6edf3messageTb4b4b4)
Tb4b4b4}
T8b949e/**
* Regression: after a cache refresh leaves stale services, the first [bleConnection.profile] throws
* [OtaProtocolException.ConnectionFailed] from `requireOtaCharacteristics`. When the cache was invalidated
* (`invalidateServiceCache()` returned true), the transport must disconnect, reconnect once, and retry the profile
* β which succeeds because the reconnect surfaces the freshly-advertised OTA characteristics.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`connect reconnects once when cache refresh leaves stale OTA services`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
T8b949e// Seed no characteristics β first profile() rejects the service as missing OTA chars.
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4, Te6edf3seedOtaCharacteristics Tff7b72= Tff7b72falseTb4b4b4)
T8b949e// Cache invalidation succeeds, arming the single reconnect retry in discoverAndPrepareOtaService().
Te6edf3connectionTb4b4b4.Te6edf3invalidateServiceCacheResult Tff7b72= Tff7b72true
T8b949e// The forced disconnect surfaces the OTA loader's real service table; seed it for the retry.
Te6edf3connectionTb4b4b4.Te6edf3onDisconnect Tff7b72= Tb4b4b4{
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3addCharacteristicTb4b4b4(Te6edf3OTA_NOTIFY_CHARACTERISTICTb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3addCharacteristicTb4b4b4(Te6edf3OTA_WRITE_CHARACTERISTICTb4b4b4)
Tb4b4b4}
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4,
Ta5d6ff"Ta5d6ffconnect() must succeed after one cache-refresh retry: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3connectionTb4b4b4.Te6edf3connectAndAwaitCalls Tff7b72>Tff7b72= T79c0ff2Tb4b4b4,
Ta5d6ff"Ta5d6ffexpected initial connect + one reconnect, got Tffd700${Te6edf3connectionTb4b4b4.Te6edf3connectAndAwaitCallsTffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e// -----------------------------------------------------------------------
T8b949e// startOta()
T8b949e// -----------------------------------------------------------------------
Tf0883e@Test
Tff7b72fun Td2a8ff`startOta sends command and succeeds on OK response`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// Pre-buffer "OK" response β the notification collector runs on Unconfined,
T8b949e// so it will synchronously push to responseChannel before startOta reads it.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff1T79c0ff0T79c0ff2T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffabc123hashTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
T8b949e// Verify command was written
Tff7b72val Te6edf3commandWrites Tff7b72= Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3writeType Tff7b72=Tff7b72= Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSE Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3commandWritesTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffShould have written at least one command packetTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3commandText Tff7b72= Te6edf3commandWritesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3decodeToStringTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3joinToStringTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3commandTextTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffOTA 1024 abc123hashTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffCommand should contain OTA start messageTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`startOta handles ERASING then OK sequence`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Tff7b72val Te6edf3handshakeStatuses Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3OtaHandshakeStatusTff7b72>Tb4b4b4(Tb4b4b4)
T8b949e// Pre-buffer both responses
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERASINGTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff2T79c0ff0T79c0ff4T79c0ff8LTb4b4b4, Ta5d6ff"Ta5d6ffhash256Ta5d6ff"Tb4b4b4) Tb4b4b4{ Te6edf3status Tff7b72-Tff7b72> Te6edf3handshakeStatusesTb4b4b4.Te6edf3addTb4b4b4(Te6edf3statusTb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3handshakeStatusesTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaHandshakeStatusTb4b4b4.Te6edf3ErasingTff7b72>Tb4b4b4(Te6edf3handshakeStatusesTff7b72[T79c0ff0Tff7b72]Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`startOta fails on Hash Rejected error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Hash RejectedTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff1T79c0ff0T79c0ff2T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffbadhashTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3HashRejectedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`startOta fails on generic error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Something went wrongTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff1T79c0ff0T79c0ff2T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffsomehashTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3CommandFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// -----------------------------------------------------------------------
T8b949e// streamFirmware()
T8b949e// -----------------------------------------------------------------------
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware sends data and succeeds with final OK`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// Complete OTA handshake
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3progressValues Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657FloatTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareData Tff7b72= Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x01Tb4b4b4, T79c0ff0Te6edf3x02Tb4b4b4, T79c0ff0Te6edf3x03Tb4b4b4, T79c0ff0Te6edf3x04Tb4b4b4)
T8b949e// 4-byte firmware, chunkSize 4, payload 512 β one chunk = one write expecting one response.
T8b949e// The terminal OK on the last (only) chunk completes the transfer.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3firmwareDataTb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{ Te6edf3progress Tff7b72-Tff7b72> Te6edf3progressValuesTb4b4b4.Te6edf3addTb4b4b4(Te6edf3progressTb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstreamFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3progressValuesTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffShould have reported progressTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1.0fTb4b4b4, Te6edf3progressValuesTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware handles multi-chunk transfer`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff8LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3progressValues Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657FloatTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareData Tff7b72= Te6edf3ByteArrayTb4b4b4(T79c0ff8Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}
T8b949e// chunkSize=4, maxWriteValueLength=512
T8b949e// Chunk 1 (bytes 0-3): 1 packet β 1 ACK
T8b949e// Chunk 2 (bytes 4-7): 1 packet β 1 OK (last chunk, last packet β early return)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3firmwareDataTb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{ Te6edf3progress Tff7b72-Tff7b72> Te6edf3progressValuesTb4b4b4.Te6edf3addTb4b4b4(Te6edf3progressTb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstreamFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3progressValuesTb4b4b4.Te6edf3size Tff7b72>Tff7b72= T79c0ff2Tb4b4b4, Ta5d6ff"Ta5d6ffShould have at least 2 progress reports, got Tffd700$Te6edf3progressValuesTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1.0fTb4b4b4, Te6edf3progressValuesTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware reuses discovered OTA service for chunk writes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3maxWriteValueLength Tff7b72= T79c0ff2T79c0ff0T79c0ff0
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profileCallsAfterConnect Tff7b72= Te6edf3connectionTb4b4b4.Te6edf3profileCalls
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff6T79c0ff0T79c0ff0LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff5T79c0ff1T79c0ff2Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstreamFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3profileCallsAfterConnectTb4b4b4, Ta5d6ff"Ta5d6ffconnect should discover the OTA profile onceTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3profileCallsAfterConnectTb4b4b4,
Te6edf3connectionTb4b4b4.Te6edf3profileCallsTb4b4b4,
Ta5d6ff"Ta5d6ffwrites should reuse the discovered OTA serviceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails on connection lost`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// Start OTA
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// Simulate connection loss β disconnect sets isConnected=false via connectionState flow
Te6edf3connectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x01Tb4b4b4, T79c0ff0Te6edf3x02Tb4b4b4, T79c0ff0Te6edf3x03Tb4b4b4, T79c0ff0Te6edf3x04Tb4b4b4)Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails on Hash Mismatch error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Hash MismatchTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x01Tb4b4b4, T79c0ff0Te6edf3x02Tb4b4b4, T79c0ff0Te6edf3x03Tb4b4b4, T79c0ff0Te6edf3x04Tb4b4b4)Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3VerificationFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails on generic transfer error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Flash write failedTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff0Te6edf3x01Tb4b4b4, T79c0ff0Te6edf3x02Tb4b4b4, T79c0ff0Te6edf3x03Tb4b4b4, T79c0ff0Te6edf3x04Tb4b4b4)Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware clamps chunk to negotiated write payload and acks once per chunk`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// Negotiated payload (200) smaller than the requested chunk (512) β the real MTU-512 case is payload 509.
Te6edf3connectionTb4b4b4.Te6edf3maxWriteValueLength Tff7b72= T79c0ff2T79c0ff0T79c0ff0
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff6T79c0ff0T79c0ff0LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// The transport must clamp 512 β 200, yielding three 200-byte chunks, each ONE write expecting ONE
T8b949e// response. Before the fix the 512-byte chunk fragmented into multiple writes and the loop waited for a
T8b949e// response per fragment, desyncing against the device's one-response-per-chunk cadence (10s ACK-timeout hang).
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3progress Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657FloatTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff5T79c0ff1T79c0ff2Tb4b4b4) Tb4b4b4{ Te6edf3progressTb4b4b4.Te6edf3addTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstreamFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1.0fTb4b4b4, Te6edf3progressTb4b4b4.Te6edf3lastTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3dataWrites Tff7b72= Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3writeType Tff7b72=Tff7b72= Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSE Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3dataWritesTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffexpected exactly one write per 200-byte chunkTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3dataWritesTb4b4b4.Te6edf3all Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3size Tff7b72<Tff7b72= T79c0ff2T79c0ff0T79c0ff0 Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffno write may exceed the negotiated payloadTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware surfaces a final-chunk error instead of reporting success`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3maxWriteValueLength Tff7b72= T79c0ff2T79c0ff0T79c0ff0
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff6T79c0ff0T79c0ff0LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// First two chunks ack; the device then rejects the final image hash. Must fail, never report success.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Hash MismatchTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff5T79c0ff1T79c0ff2Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3VerificationFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware succeeds when the last chunk is ACKed then a separate terminal OK arrives`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff8LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// Last chunk is ACKed (not OK); the device then sends a separate terminal OK. Exercises the post-loop
T8b949e// verification wait, which must complete successfully.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff8Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstreamFirmware failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails when a terminal error arrives after the last chunk is ACKed`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff8LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// All chunks ACKed, then the device rejects the image post-transfer. Exercises the post-loop error branch β
T8b949e// a late error must surface as failure, never success.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffACKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffERR Hash MismatchTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff8Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3VerificationFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails when a non-final chunk receives OK`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff8LTb4b4b4, Ta5d6ff"Ta5d6ffhashTa5d6ff"Tb4b4b4)
T8b949e// A premature OK on the first of two chunks: the device sends OK only at completion, so this signals a
T8b949e// size disagreement and must fail rather than be treated as an ACK.
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff8Tb4b4b4) Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4, T79c0ff4Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`streamFirmware fails immediately on empty firmware`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
T8b949e// Empty image: must fail right away rather than skipping the loop and waiting out VERIFICATION_TIMEOUT. No
T8b949e// device response is buffered, so a regression (waiting for a response) would surface as a Timeout, not this.
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3streamFirmwareTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4, T79c0ff5T79c0ff1T79c0ff2Tb4b4b4) Tb4b4b4{Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isFailureTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3OtaProtocolExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// -----------------------------------------------------------------------
T8b949e// close()
T8b949e// -----------------------------------------------------------------------
Tf0883e@Test
Tff7b72fun Td2a8ff`close disconnects BLE connection`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3connectionTb4b4b4.Te6edf3disconnectCallsTb4b4b4)
Tb4b4b4}
T8b949e// -----------------------------------------------------------------------
T8b949e// writeData chunking
T8b949e// -----------------------------------------------------------------------
Tf0883e@Test
Tff7b72fun Td2a8ff`startOta splits command across MTU-sized packets`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3scanner Tff7b72= Te6edf3FakeBleScannerTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connection Tff7b72= Te6edf3FakeBleConnectionTb4b4b4(Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3transportTb4b4b4) Tff7b72= Te6edf3createTransportTb4b4b4(Te6edf3scannerTb4b4b4, Te6edf3connectionTb4b4b4)
Te6edf3connectionTb4b4b4.Te6edf3maxWriteValueLength Tff7b72= T79c0ff1T79c0ff0
Te6edf3scannerTb4b4b4.Te6edf3emitDeviceTb4b4b4(Te6edf3FakeBleDeviceTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4)
Te6edf3transportTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getOrThrowTb4b4b4(Tb4b4b4)
T8b949e// "OTA 1024 abc123hash\n" is 21 bytes β with maxLen=10, needs 3 packets, so 3 OK responses
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Te6edf3emitResponseTb4b4b4(Te6edf3connectionTb4b4b4, Ta5d6ff"Ta5d6ffOKTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3transportTb4b4b4.Te6edf3startOtaTb4b4b4(T79c0ff1T79c0ff0T79c0ff2T79c0ff4LTb4b4b4, Ta5d6ff"Ta5d6ffabc123hashTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3isSuccessTb4b4b4, Ta5d6ff"Ta5d6ffstartOta failed: Tffd700${Te6edf3resultTb4b4b4.Te6edf3exceptionOrNullTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
T8b949e// Verify the command was split into multiple writes
Tff7b72val Te6edf3commandWrites Tff7b72= Te6edf3connectionTb4b4b4.Te6edf3serviceTb4b4b4.Te6edf3writesTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3writeType Tff7b72=Tff7b72= Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITH_RESPONSE Tb4b4b4}
Te6edf3assertTrueTb4b4b4(
Te6edf3commandWritesTb4b4b4.Te6edf3size Tff7b72> T79c0ff1Tb4b4b4,
Ta5d6ff"Ta5d6ffCommand should be split into multiple MTU-sized packets, got Tffd700${Te6edf3commandWritesTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff"Tb4b4b4,
Tb4b4b4)
T8b949e// Verify reassembled command content
Tff7b72val Te6edf3reassembled Tff7b72= Te6edf3commandWritesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3decodeToStringTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4.Te6edf3joinToStringTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffOTA 1024 abc123hashTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3reassembledTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.15s